Skip to content

feat(objectql): publish the registry's three conflict error codes as importable constants - #16259

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16159-registry-error-code-constants
Sep 6, 2026
Merged

feat(objectql): publish the registry's three conflict error codes as importable constants#16259
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16159-registry-error-code-constants

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Part of #16159

The first slice of that card's sweep: the three rows in packages/objectql/src/registry.ts only — NAMESPACE_CONFLICT, DUPLICATE_ARTIFACT_OBJECT_NAME, OBJECT_OWNERSHIP_CONFLICT. Part of, not Fixes: the card lists further rows in four other files, and this PR discharges three of them. It is deliberately not a batch conversion — each row is a published-surface widening, so each slice is its own Clause-② decision and its own minor changeset, exactly as the card argued.

⛔ Out of scope and untouched: every other row, and in particular validation/record-validator.ts. Its VALIDATION_FAILED versus the already-exported EMPTY_CREDENTIAL_REFUSAL_CODE = 'VALIDATION_ERROR' is an unresolved naming question, and promoting either to a published constant would freeze that disagreement into the surface. #16159 remains open.

What changed

Three inline string literals became exported *_CODE constants on the shape the six existing constants in this package already use, re-exported from the index.ts barrel:

constant value refusal
NAMESPACE_CONFLICT_CODE NAMESPACE_CONFLICT ADR-0048 Phase 1 install-time namespace gate
DUPLICATE_ARTIFACT_OBJECT_NAME_CODE DUPLICATE_ARTIFACT_OBJECT_NAME ADR-0130 D3, one artifact
OBJECT_OWNERSHIP_CONFLICT_CODE OBJECT_OWNERSHIP_CONFLICT ADR-0029 D3, single owner per name

The strings did not change, and that is measured rather than asserted. Each constant holds text byte-identical to the literal it replaces, compared programmatically against the base blob. In the built artifacts each of the three wire strings appears exactly once in dist/index.js and once in dist/index.mjs, on both sides of the change — the conversion did not add a second spelling to the bundle.

The error classes stay unexported, deliberately. Publishing them would publish the instanceof route this convention exists to replace: @objectstack/objectql declares both realms in its own exports, so a consumer holding the other realm's copy gets instanceof === false, silently (#14936). The constants are the affordance; the classes are not.

Not on the lean core.ts entry — measured: all six existing *_CODE constants are on index.ts only, and core.d.ts's exported-name count is unchanged at 79 either side. Whether a @objectstack/objectql/core consumer should also reach these is a real question, but it is true of all six existing constants too, so it belongs to the sweep and not to this PR.

The one thing that was NOT neutral, and how it was found

The card's dispatch asked whether check:error-code-provenance changes what it counts. Measured both gates rather than assumed:

  • check:error-code-provenance is neutral by construction, not merely green. scanSourceText does if (!registered.has(code)) continue;, and none of these three codes is in the ledger — 0 hits each in error-code-ledger.zod.ts against firing controls of ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED (2) and VALIDATION_ERROR (3) on the same command and file. A new constdef for an unregistered code cannot enter that population.
  • check:dispatcher-error-vocabulary was NOT neutral, and went red. That gate reconciles in both directions and had all three rows declared as shape: 'classfield'. Converting the literal to a constant moves the site to the classconst recogniser, so the gate returned exit 1 with six findings — three stale-row plus three unclassified-site — until the three rows in packages/runtime/src/dispatcher-error-vocabulary.ts were updated to shape: 'classconst'. Baseline and final both read 66 unregistered code-stamping sites, 66 classified: the census is unchanged, only the shape a site is matched by moved. Each row's door and verdict are untouched.

That runtime file is not exported from @objectstack/runtime's barrel, and its two published declarations are byte-identical hashes across the ablation (152 exported names either side) — so runtime publishes nothing new and owes no changeset entry. Measured, not inferred.

Clause-② — yes, measured

Built at head, swapped the three sources back to the base commit a3bbb8b1a, rebuilt, and compared every declaration file each package publishes (files: ["dist", …]), not the root barrel alone — including the content-hashed shared chunk, which is where a previous round's real surface change hid. The rebuild is proven to have re-run (dist/index.js mtimes moved on both packages).

Comparing exported name sets rather than bytes, because the byte diff is dominated by tsup's alias renumbering and a changed chunk filename, neither of which is a surface change:

declaration base head added removed
objectql/dist/index.d.ts 244 247 the three constants none
objectql/dist/core.d.ts 79 79 none none
objectql shared chunk 130 133 3 internal aliases none
runtime/dist/index.d.ts 152 152 none none
runtime/dist/index.d.cts 152 152 none none

Additive widening of a published surface, nothing removed ⇒ Clause-② yes and a minor changeset. needs:contract-review hung on both carriers by the PR creator.

Tests

New packages/objectql/src/registry-conflict-code-constants.test.ts — six cases. Each refusal is asserted on its code and status envelope, never a bare toThrow(): #14367 measured on this very path that a throw-shaped assertion stayed green with the check one layer up ablated, because a second refusal fired one step later and was indistinguishable.

The existing rejection tests keep spelling the wire strings as literals on purpose — the test layer is outside check:error-code-provenance's scanned population, so those literals cost no stamp site while acting as an independent pin that a silent rename of a constant cannot pass.

Two ablations, each committed first, each proving the mutation on disk before measuring and the restore byte-exact after:

  • mutating NAMESPACE_CONFLICT_CODE's value turns exactly 2 of 6 cases red — the wire-string pin and the cross-realm control. The four that compare against the constant stay green, which is precisely why case 1 spells the literal.
  • deleting the barrel re-export turns exactly 1 of 6 red — the case that pins the affordance a consumer actually reaches.

Why the sweep should continue

Worth recording, and not repaired here because it is a different row: content/docs/kernel/contracts/data-engine.mdx:361 tells readers "Catch it by code, not instanceof" about ReadonlyFieldRejectedError — whose code is still an inline literal at readonly-strict-errors.ts:161. Confirmed still true on today's main. The published guidance and the published surface disagree today, in the documentation's own words, and that row is the strongest argument for finishing the sweep.

Also re-measured for these three specifically: none of the three codes or constants appears anywhere in content/docs (0 files each, against firing controls of @objectstack/objectql on 12 files and ReadonlyFieldRejectedError on 1). No page enumerates them; none goes stale by adding one.

Census reconciliation

The card's table and triage's independent re-count (comment 5556857711) disagreed on one row, and triage was right when it was written and is now stale: it recorded system-write-organization.ts as still an inline literal, making 12 rather than 11. On today's main #16156 has landed (48b0fcf24 is an ancestor) and that site reads SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE. So the live count on main before this PR is 10 inline literals, and this PR takes 3 of them, leaving 7. Triage's note ① also holds: readonly code = finds 16, and nav-contribution-diagnostics.ts:93 is the 17th carrier under a typeof spelling.

⛔ The card's line numbers are stale for two of the three rows (:1404/:1463 are now :1406/:1467); everything here was located by text.

The template

This is the first slice, so the shape is meant to be copied rather than admired: one shared docblock carrying the "by code, not instanceof" reasoning once, three one-line constants beside the classes they belong to, one barrel re-export block, one pin test, one minor changeset — and a check of whether the row has a dispatcher-error-vocabulary declaration whose shape must move with it. That last step is the one a mechanical repeat of this conversion will miss.

Verification

Everything below ran against 5ca781835, the commit this PR carries, on a clean tree.

  • pnpm --filter @objectstack/objectql exec vitest run276 files / 4755 tests passed
  • pnpm --filter @objectstack/runtime exec vitest run234 files / 3318 tests passed
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime typecheck — green, test layer included; the new test file is proven inside tsconfig.test.json's program via --listFiles, contributing 0 of the 242 pre-existing debt errors
  • 57 of 57 derived gate families from scripts/pm/dispatch-gates.mjs --commands, plus check:error-code-provenance, check:error-code-casing, check:error-status-conformance, check:authz-resolver, check:filter-alias-parity — all exit 0, captured with redirect-then-read, never through a pipe
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, run in full rather than narrowed
  • check:dual-build-cjs-loads and check:type-check-debt first returned exit 3, PREREQUISITE NOT MET — reported here because that is not a pass. The full workspace closure was then built as lint.yml does and both re-run to a real exit 0; type-check-debt re-measured 12 ledger entries, 140 raw errors, none above its recorded number.

⚠️ PR #16232 is also open on packages/objectql/src/ and touches index.ts at line ~144. This PR's re-export sits in the registry block at lines 68–85, so the two do not overlap textually. This branch was not rebased onto it and none of its hunks were touched; if a conflict appears at merge time it should go back to the respective claims rather than be resolved here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ


Generated by Claude Code

…importable constants

`NamespaceConflictError`, `ArtifactObjectNameConflictError` and
`ObjectOwnershipConflictError` each already tell the reader to identify them by
`code` rather than `instanceof`, and offered nothing to import. Convert the
three inline literals to exported `*_CODE` constants on the shape the six
existing constants in this package already use, and re-export them from the
package barrel.

The strings are byte-identical to the literals they replace: this moves where a
spelling lives, never what it says.

The three rows in `packages/runtime/src/dispatcher-error-vocabulary.ts` move
from `shape: 'classfield'` to `shape: 'classconst'` — measured, not assumed:
`check:dispatcher-error-vocabulary` reconciles in both directions and went red
with 3 stale-row plus 3 unclassified-site findings until the rows were updated.
The site census is unchanged at 66/66 classified either side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added the size/m label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 60c0f6134680c23cafb2f85185e64634ca3a4080packageMentionDocs.

Which tree this was computed on

This run read content/docs from be82d967f34581aad469890b158a8044f75f1bcd — the merge of head 5ca78183503e186b61c7b5daa029a577ee52292a into base 60c0f6134680c23cafb2f85185e64634ca3a4080, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin be82d967f34581aad469890b158a8044f75f1bcd && git checkout be82d967f34581aad469890b158a8044f75f1bcd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 60c0f6134680c23cafb2f85185e64634ca3a4080 5ca78183503e186b61c7b5daa029a577ee52292a && git checkout -B drift-repro 60c0f6134680c23cafb2f85185e64634ca3a4080 && git merge --no-ff 5ca78183503e186b61c7b5daa029a577ee52292a

node scripts/docs-audit/affected-docs.mjs --json 60c0f6134680c23cafb2f85185e64634ca3a4080

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Copy link
Copy Markdown
Contributor

Contract review (clause ②) — PASS on content · landable on green CI — PR #16259 at head 5ca78183 (Part of #16159, slice 1 of the sweep)

Reviewed by the director seat at tier (claude-fable-5-1, session session_01TezFG8ZMrNH6n5VTNpPpdH), 2026-09-06 08:38Z, on the domain:engine seat's hand-off (5558027035: "clause-② is yes and this seat cannot review it").

Clause ② answer: yes, mechanical floor, additive — exactly as declared and measured. @objectstack/objectql's root barrel gains three exported constants (NAMESPACE_CONFLICT_CODE, DUPLICATE_ARTIFACT_OBJECT_NAME_CODE, OBJECT_OWNERSHIP_CONFLICT_CODE); the round's exported-name-set comparison across every published declaration file reads 244 → 247 on index.d.ts, core.d.ts unchanged at 79, the shared chunk's +3 are internal aliases, and @objectstack/runtime's two declarations are hash-identical (152 either side) — so runtime publishes nothing and correctly owes no changeset entry. Nothing removed or renamed. Comparing name sets rather than bytes is the right instrument here and is adopted for future reviews: a tsup alias renumbering is not a surface move.

Content read on the diff: the three literals become as const constants beside their classes and the class fields read from them — wire strings byte-identical, status / httpStatus 422 unchanged; the classes stay unexported, deliberately (publishing them would publish the instanceof route the convention replaces, and #14936 measured why instanceof fails across this package's two realms). record-validator.ts untouched, per the fence. The cross-package edit to dispatcher-error-vocabulary.ts is a gate's prescription, not scope creep: check:dispatcher-error-vocabulary reconciles both ways and went red (3 stale-row + 3 unclassified-site) until the three rows moved classfield → classconst; census 66/66 unchanged, door and verdict untouched. check:error-code-provenance is neutral by construction (if (!registered.has(code)) continue;, with firing controls).

Tests read (registry-conflict-code-constants.test.ts, 107 lines): wire-string pin spelled literally on purpose; code + status + httpStatus per refusal, never a bare toThrow (#14367); barrel identity, not equality; the foreign-realm control that makes the whole convention falsifiable.

Changeset: @objectstack/objectql: minor — correct for an additive published-surface widening; no ADR-0087 marker owed. Governed-merge audit on the 5 paths: 0 hits. CI at 5ca78183: 28 success · 6 skipped · 2 still running.

Carriers: needs:contract-review comes off this PR and card #16159 now for this slice; the card stays open and pm:dispatched for the remaining rows, and each future slice re-hangs its own review on both carriers when its diff exists. On green CI the next director pass runs --pair 16259 and flips ready-for-review + auto-merge (squash).


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 6, 2026 08:48
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit e6279dc Sep 6, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-16159-registry-error-code-constants branch September 6, 2026 09:37
zhuangjianguo pushed a commit that referenced this pull request Sep 6, 2026
…table constant

`content/docs/kernel/contracts/data-engine.mdx` already tells readers, of this
exact refusal, to "Catch it by `code`, not `instanceof`, and read `drops` for
the per-reason breakdown" — and the code was an inline string literal with
nothing to import. The published guidance and the published surface disagreed,
in the documentation's own words; that is why this row of #16159's table was
converted ahead of the latent ones.

`READONLY_FIELD_REJECTED_CODE` is a new export from `@objectstack/objectql`,
re-exported from `index.ts` and deliberately not from the lean `core.ts` entry,
matching the `*_CODE` constants already in this package. Dropping the `ERR_`
prefix from the constant's NAME follows the two `ERR_`-prefixed precedents here
(`HOOK_TARGET_REBIND_ERROR_CODE`, `SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE`).

The string is byte-identical to the literal it replaces: the quoted spelling
occurs exactly once in the file on both sides of the change — it moved, it did
not multiply or mutate.

Two gate readings, measured rather than assumed, and the second contradicts
what was expected of it:

  - `check:error-code-provenance` is NOT neutral here. Unlike the codes
    converted on #16259, `ERR_READONLY_FIELD_REJECTED` IS in `ERROR_CODE_LEDGER`
    (the gate skips unregistered codes), so the new constant is a `constdef`
    stamp site the gate sees: 310 -> 311 sites, 294 -> 295 listed, waivers
    unchanged at 16, exit 0 both sides. It passes because the code is listed
    under this package's own owner key.

  - `check:dispatcher-error-vocabulary` does NOT move, and no row in
    `packages/runtime/src/dispatcher-error-vocabulary.ts` changes. That table
    records UNREGISTERED code sites (`return !registered.has(value)`), and this
    code is registered, so the site is invisible to it by construction — there
    is no row for it to move. 66/66 classified either side, exit 0. No
    cross-package edit is owed for this row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants